/*
 * L2TP/IPsec VPN plugin.
 *
 * Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>

#include <stdlib.h>
#include <stdio.h>
#include <net/if.h>
#include <netdb.h>

#include <glib.h>

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/plugin.h>
#include <connman/dbus.h>
#include <connman/provider.h>
#include <connman/log.h>
#include <connman/task.h>
#include <connman/inet.h>

#ifdef ENABLE_NSS
#include "nss.h"
#endif

#include "vpn.h"

#define	_DBG_VPN(fmt, arg...)	DBG(DBG_VPN, fmt, ## arg)

/* Exit status of l2tpipsec_vpn defined in vpn-manager/service_error.h */
enum l2tpipsec_exit_status {
	L2TPIPSEC_ERROR_NO_ERROR                = 0,

	/* Common errors */
	L2TPIPSEC_ERROR_INTERNAL                = 1,
	L2TPIPSEC_ERROR_INVALID_ARGUMENT        = 2,
	L2TPIPSEC_ERROR_RESOLVE_HOSTNAME_FAILED = 3,

	/* IPsec specific errors */
	L2TPIPSEC_ERROR_IPSEC_CONNECTION_FAILED = 32,
	L2TPIPSEC_ERROR_IPSEC_PSK_AUTH_FAILED   = 33,
	L2TPIPSEC_ERROR_IPSEC_CERT_AUTH_FAILED  = 34,

	/* LT2P specific errors */
	L2TPIPSEC_ERROR_L2TP_CONNECTION_FAILED  = 64,

	/* PPP specific errors */
	L2TPIPSEC_ERROR_PPP_CONNECTION_FAILED   = 128,
	L2TPIPSEC_ERROR_PPP_AUTH_FAILED         = 129
};

/* Structure used to store any L2TP/IPsec VPN-specific data. */
struct l2tpipsec_data {
	/* Path to file containing PSK or NULL if PSK not needed. */
	gchar *psk_file;
        connman_bool_t added_host_route;
        in_addr_t vpn_addr;
        in_addr_t gw_addr;
        uint32_t gw_index;
};

static DBusConnection *connection;

static const char *kPSKProperty = "L2TPIPsec.PSK";
static const char *kPasswordProperty = "L2TPIPsec.Password";
static const char *kUserProperty = "L2TPIPsec.User";

static DBusMessage *li_get_sec(struct connman_task *task,
			DBusMessage *msg, void *user_data)
{
	const char *user, *passwd;
	struct connman_provider *provider = user_data;

	if (dbus_message_get_no_reply(msg) == FALSE) {
		DBusMessage *reply;

		user = connman_provider_get_string(provider, kUserProperty);
		passwd = connman_provider_get_string(provider,
						     kPasswordProperty);

		if (user == NULL || strlen(user) == 0) {
			_DBG_VPN("User not set");
			return NULL;
		}
		if (passwd == NULL || strlen(passwd) == 0) {
			_DBG_VPN("Password not set");
			return NULL;
		}

		reply = dbus_message_new_method_return(msg);
		if (reply == NULL)
			return NULL;

		dbus_message_append_args(reply, DBUS_TYPE_STRING, &user,
						DBUS_TYPE_STRING, &passwd,
						DBUS_TYPE_INVALID);

		return reply;
	}

	return NULL;
}

static void remove_psk_file(struct connman_provider *provider)
{
	struct l2tpipsec_data *specific_data;

        specific_data = vpn_get_specific_data(provider);
	if (specific_data == NULL)
		return;
	if (specific_data->psk_file == NULL)
		return;
	unlink(specific_data->psk_file);
	g_free(specific_data->psk_file);
	specific_data->psk_file = NULL;
}

static int li_notify(DBusMessage *msg, struct connman_provider *provider)
{
	DBusMessageIter iter, dict;
	const char *reason, *key;
	char *value;
	char *ifname = NULL;
	char *dns_servers[3];
	struct connman_ipaddress ipaddr;
        char *remote_server_address = NULL;

	dbus_message_iter_init(msg, &iter);

	dbus_message_iter_get_basic(&iter, &reason);
	dbus_message_iter_next(&iter);

	_DBG_VPN("Reason %s", reason);

	if (provider == NULL) {
		connman_error("%s: No provider found", __func__);
		return VPN_STATE_FAILURE;
	}

	if (strcmp(reason, "connect"))
		return VPN_STATE_DISCONNECT;

	memset(&ipaddr, 0, sizeof(ipaddr));
	ipaddr.af = AF_INET;
	ipaddr.mask |= CONNMAN_IPCONFIG_AF;
	memset(dns_servers, 0, sizeof(dns_servers));
	ipaddr.dns_servers = dns_servers;

	dbus_message_iter_recurse(&iter, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);
		dbus_message_iter_next(&entry);
		dbus_message_iter_get_basic(&entry, &value);

		_DBG_VPN("%s = %s", key, value);

		if (!strcmp(key, "INTERNAL_IP4_ADDRESS")) {
			ipaddr.local = value;
			ipaddr.mask |= CONNMAN_IPCONFIG_LOCAL;
		} else if (!strcmp(key, "EXTERNAL_IP4_ADDRESS")) {
			ipaddr.peer = value;
			ipaddr.mask |= CONNMAN_IPCONFIG_PEER;
		} else if (!strcmp(key, "GATEWAY_ADDRESS")) {
			ipaddr.gateway = value;
			ipaddr.mask |= CONNMAN_IPCONFIG_GW;
			/*
			 * Set the prefixlen so that dns configuration occurs
			 * and host routes are enabled.  The kernel expects
			 * PPP device netmasks to be 32b, otherwise it removes
			 * host routes that it configures.
			 */
			ipaddr.prefixlen = 32;
			ipaddr.mask |= CONNMAN_IPCONFIG_PREFIX;
		} else if (!strcmp(key, "DNS1")) {
			ipaddr.dns_servers[0] = value;
			ipaddr.mask |= CONNMAN_IPCONFIG_DNS;
		} else if (!strcmp(key, "DNS2")) {
			ipaddr.dns_servers[1] = value;
		} else if (!strcmp(key, "INTERNAL_IFNAME")) {
			ifname = value;
		} else if (!strcmp(key, "LNS_ADDRESS")) {
			remote_server_address = value;
                }

		dbus_message_iter_next(&dict);
	}

	if (vpn_set_ifname(provider, ifname) < 0) {
		return VPN_STATE_FAILURE;
	}

        if (ipaddr.mask & CONNMAN_IPCONFIG_GW) {
                /*
                 * If we are creating a new default gateway, we need
                 * to first pin a host route to the VPN server so that
                 * encapsulated packets continue to be sent to the VPN
                 * server endpoint.  Note that we do not need to
                 * observe default gateway changes since IPsec v1
                 * which is used in L2TP over IPsec itself is not able
                 * to handle switching of underlying networks.
                 */
                struct l2tpipsec_data *specific_data;
                if (remote_server_address == NULL) {
                        connman_error("%s: request to set gw without "
                                      "LNS_ADDRESS", __func__);
                        return VPN_STATE_FAILURE;
                }
                _DBG_VPN("Adding host VPN server route");
                specific_data = vpn_get_specific_data(provider);
                if (!specific_data) {
                        connman_error("%s: no specific data", __func__);
                        return VPN_STATE_FAILURE;
                }
                specific_data->vpn_addr = inet_addr(remote_server_address);

                if (!connman_inet_get_route(specific_data->vpn_addr,
                                            &specific_data->gw_index,
                                            &specific_data->gw_addr,
                                            NULL)) {
                        connman_error("%s: unable to get route", __func__);
                        return VPN_STATE_FAILURE;
                }
                connman_inet_add_hostroute(
                    specific_data->gw_index, specific_data->vpn_addr,
                    specific_data->gw_addr);
                specific_data->added_host_route = TRUE;
        }
	connman_provider_ipconfig_set(provider, &ipaddr);

	remove_psk_file(provider);

	return VPN_STATE_CONNECT;
}

static enum connman_provider_error exit_status_to_error(gint status)
{
	if (WIFEXITED(status)) {
		switch (WEXITSTATUS(status)) {
		case L2TPIPSEC_ERROR_NO_ERROR:
			return CONNMAN_PROVIDER_ERROR_NO_ERROR;

		case L2TPIPSEC_ERROR_RESOLVE_HOSTNAME_FAILED:
			return CONNMAN_PROVIDER_ERROR_RESOLVE_HOSTNAME_FAILED;

		case L2TPIPSEC_ERROR_IPSEC_CONNECTION_FAILED:
		case L2TPIPSEC_ERROR_L2TP_CONNECTION_FAILED:
		case L2TPIPSEC_ERROR_PPP_CONNECTION_FAILED:
			return CONNMAN_PROVIDER_ERROR_CONNECT_FAILED;

		case L2TPIPSEC_ERROR_IPSEC_PSK_AUTH_FAILED:
			return CONNMAN_PROVIDER_ERROR_IPSEC_PSK_AUTH_FAILED;
		case L2TPIPSEC_ERROR_IPSEC_CERT_AUTH_FAILED:
			return CONNMAN_PROVIDER_ERROR_IPSEC_CERT_AUTH_FAILED;
		case L2TPIPSEC_ERROR_PPP_AUTH_FAILED:
			return CONNMAN_PROVIDER_ERROR_PPP_AUTH_FAILED;

		default:
			return CONNMAN_PROVIDER_ERROR_INTERNAL;
		}
	}
	return CONNMAN_PROVIDER_ERROR_INTERNAL;
}

static void li_vpn_died(struct connman_task *task, void *user_data)
{
	struct connman_provider *provider = user_data;
	struct l2tpipsec_data *specific_data;
	gint status = connman_task_get_exit_status(task);
	enum connman_provider_error error = exit_status_to_error(status);

	remove_psk_file(provider);
	specific_data = vpn_get_specific_data(provider);
	if (specific_data && specific_data->added_host_route) {
		_DBG_VPN("Tearing down host VPN server route");
		connman_inet_del_hostroute(specific_data->gw_index,
					   specific_data->vpn_addr,
					   specific_data->gw_addr);
		specific_data->added_host_route = FALSE;
	}
	vpn_set_specific_data(provider, NULL);
	g_free(specific_data);
	vpn_died(task, user_data, error);
}

static int init_specific_data(struct connman_provider *provider)
{
	struct l2tpipsec_data *specific_data;

	specific_data = g_try_new0(struct l2tpipsec_data, 1);
	if (specific_data == NULL) {
		connman_error("%s: out of memory", __func__);
		return -ENOMEM;
	}
	vpn_set_specific_data(provider, specific_data);
        return 0;
}

static int create_psk_file(struct connman_provider *provider,
		const char *psk,
		const char **psk_file)
{
	struct l2tpipsec_data *specific_data = NULL;
	char psk_file_template[] = "/var/run/flimflam/l2tpipsec_XXXXXX";
	int psk_file_fd;

	/* mkstemp assures 0600 file permissions. */
	psk_file_fd = mkstemp(psk_file_template);
	if (psk_file_fd < 0) {
		connman_error("%s: mkstemp failed", __func__);
		return -EIO;
	}

	if (write(psk_file_fd, psk, strlen(psk)) != strlen(psk)) {
		connman_error("%s: bad write", __func__);
		return -EIO;
	}

	close(psk_file_fd);

	specific_data = vpn_get_specific_data(provider);

	specific_data->psk_file = g_strdup(psk_file_template);

	*psk_file = specific_data->psk_file;
	return 0;
}

static int li_connect(struct connman_provider *provider,
		struct connman_task *task, const char *if_name)
{
#define	OPT_STR(property, option) do {					 \
	const char *s = connman_provider_get_string(provider, property); \
	if (s != NULL)							 \
		connman_task_add_argument(task, option, "%s", s);	 \
} while (0)
#define	OPT_BOOL(property, true_option, false_option) do {		 \
	const char *s = connman_provider_get_string(provider, property); \
	if (s != NULL) {						 \
		connman_task_add_argument(task, strcmp("true", s) == 0 ? \
					true_option : false_option,	 \
					NULL);				 \
	}								 \
} while (0)
	const char *vpnhost;
	const char *value;
	const char *psk_file;
	int err, fd;

	if (init_specific_data(provider) < 0)
		return -ENOMEM;

	if (connman_task_set_notify(task, "getsec",
					li_get_sec, provider))
		return -ENOMEM;

	vpnhost = connman_provider_get_string(provider, CONNMAN_PROVIDER_HOST);
	if (!vpnhost) {
		connman_error("%s: host not set; cannot enable VPN", __func__);
		return -EINVAL;
	}

	value = connman_provider_get_string(provider, kPSKProperty);
	if (value != NULL && value[0] != '\0') {
		if (create_psk_file(provider, value, &psk_file) < 0) {
			connman_error("%s: unable to write psk file", __func__);
		} else {
			connman_task_add_argument(task, "--psk_file",
			    "%s", psk_file);
		}
	}

	connman_task_add_argument(task,	      "--remote_host",
					"%s", vpnhost);
	connman_task_add_argument(task,	      "--pppd_plugin",
					"%s", SCRIPTDIR "/libppp-plugin.so");
	/* Disable pppd from configuring IP addresses, routes, dns. */
	connman_task_add_argument(task, "--nosystemconfig", NULL);

#ifdef ENABLE_NSS
	value = connman_provider_get_string(provider, "L2TPIPsec.CACertNSS");
	if (value != NULL) {
		char *filename;
		filename = nss_get_der_certfile(value, (uint8_t *)vpnhost,
						strlen(vpnhost));
		if (filename != NULL) {
			connman_task_add_argument(task, "--server_ca_file",
			    "%s", filename);
		}
		g_free(filename);
	}
#endif
	OPT_STR("L2TPIPsec.ClientCertID",     "--client_cert_id");
	OPT_STR("L2TPIPsec.ClientCertSlot",   "--client_cert_slot");
        OPT_STR("L2TPIPsec.PIN",              "--user_pin");
	OPT_STR("L2TPIPsec.User",	      "--user");
	OPT_STR("L2TPIPsec.IPsecTimeout",     "--ipsec_timeout");
	OPT_STR("L2TPIPsec.LeftProtoPort",    "--leftprotoport");
	OPT_BOOL("L2TPIPsec.PFS",	      "--pfs", "--nopfs");
	OPT_BOOL("L2TPIPsec.Rekey",	      "--rekey", "--norekey");
	OPT_STR("L2TPIPsec.RightProtoPort",   "--leftprotoport");

	OPT_BOOL("L2TPIPsec.RequireChap",     "--require_chap",
					"--norequire_chap");
	OPT_BOOL("L2TPIPsec.RefusePap",	      "--refuse_pap", "--norefuse_pap");
	OPT_BOOL("L2TPIPsec.RequireAuth",     "--require_authentication",
					"--norequire_authentication");
	OPT_BOOL("L2TPIPsec.LengthBit",	      "--length_bit", "--nolength_bit");
	if (connman_debug_enabled(DBG_VPN) == TRUE)
		connman_task_add_argument(task, "--debug", NULL);

	fd = fileno(stderr);
	err = connman_task_run(task, li_vpn_died, provider,
				NULL, &fd, &fd);
	if (err < 0) {
		connman_error("l2tpipsec failed to start");
		return -EIO;
	}

	return 0;
}

static const char *li_public_props[] = {
	"L2TPIPsec.CACertNSS",
	"L2TPIPsec.ClientCertID",
	"L2TPIPsec.ClientCertSlot",
	"L2TPIPsec.IPsecTimeout",
	"L2TPIPsec.LeftProtoPort",
	"L2TPIPsec.LengthBit",
	"L2TPIPsec.PFS",
	"L2TPIPsec.RefusePap",
	"L2TPIPsec.Rekey",
	"L2TPIPsec.RequireAuth",
	"L2TPIPsec.RequireChap",
	"L2TPIPsec.RightProtoPort",
	"L2TPIPsec.User",
	NULL
};

/*
 * Append plugin-specific properties to the D-Bus dictionary.
 */
void li_append_props(struct connman_provider *provider, DBusMessageIter *iter,
    connman_bool_t isprivileged)
{
	static const char *li_priv_props[] = {
		"L2TPIPsec.Password",
                "L2TPIPsec.PIN",
		"L2TPIPsec.PSK",
		NULL
	};
	dbus_bool_t required;

	connman_provider_append_properties(provider, li_public_props, iter);
	if (isprivileged)
		connman_provider_append_properties(provider, li_priv_props,
		    iter);

	required = connman_provider_property_is_empty(provider,
							kPasswordProperty);
	connman_dbus_dict_append_basic(iter, "PassphraseRequired",
					DBUS_TYPE_BOOLEAN, &required);
	required = connman_provider_property_is_empty(provider,
							kPSKProperty);
	connman_dbus_dict_append_basic(iter, "L2TPIPsec.PSKRequired",
					DBUS_TYPE_BOOLEAN, &required);
}

/*
 * Load plugin-specific properties from the profile.
 */
static void li_load_props(struct connman_provider *provider, GKeyFile *keyfile)
{
	connman_provider_load_save_properties(
	    provider, li_public_props, connman_provider_load_property, keyfile);
	connman_provider_load_encrypted_property(provider, kPSKProperty,
	    keyfile);
	connman_provider_load_encrypted_property(provider, kPasswordProperty,
	    keyfile);
}

/*
 * Save plugin-specific properties to the profile.
 */
static void li_save_props(struct connman_provider *provider, GKeyFile *keyfile)
{
	connman_provider_load_save_properties(
	    provider, li_public_props, connman_provider_save_property, keyfile);
	connman_provider_save_encrypted_property(provider, kPSKProperty,
	    keyfile);
	connman_provider_save_encrypted_property(provider, kPasswordProperty,
	    keyfile);
}

static struct vpn_driver vpn_driver = {
	.flags		= VPN_FLAG_NO_TUN,
	.notify		= li_notify,
	.connect	= li_connect,
	.append_props	= li_append_props,
	.load_props	= li_load_props,
	.save_props	= li_save_props,
};

static void __mask_value_of_keys(void)
{
	connman_log_mask_value_of_key(kPSKProperty);
	connman_log_mask_value_of_key(kPasswordProperty);
	connman_log_mask_value_of_key(kUserProperty);
}

static int li_init(void)
{
	connection = connman_dbus_get_connection();

	__mask_value_of_keys();

	return vpn_register("l2tpipsec", &vpn_driver, L2TPIPSEC);
}

static void li_exit(void)
{
	vpn_unregister("l2tpipsec");

	dbus_connection_unref(connection);
}

CONNMAN_PLUGIN_DEFINE(l2tpipsec, "l2tpipsec plugin", VERSION,
	CONNMAN_PLUGIN_PRIORITY_DEFAULT, li_init, li_exit)
